/*
 (c) Copyrights 2007 - 2008

 Original idea by by Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/

 jQuery Plugin by Tzury Bar Yochay
 tzury.by@gmail.com
 http://evalinux.wordpress.com
 http://facebook.com/profile.php?id=513676303

 Project's sites:
 http://code.google.com/p/js-hotkeys/
 http://github.com/tzuryby/hotkeys/tree/master

 License: same as jQuery license.

 USAGE:
 // simple usage
 $(document).bind('keydown', 'Ctrl+c', function(){ alert('copy anyone?');});

 // special options such as disableInIput
 $(document).bind('keydown', {combi:'Ctrl+x', disableInInput: true} , function() {});

 Note:
 This plugin wraps the following jQuery methods: $.fn.find, $.fn.bind and $.fn.unbind
 */
(function (jQuery) {
	// keep reference to the original $.fn.bind, $.fn.unbind and $.fn.find
	jQuery.fn.__bind__ = jQuery.fn.bind;
	jQuery.fn.__unbind__ = jQuery.fn.unbind;
	jQuery.fn.__find__ = jQuery.fn.find;

	var hotkeys = {
		version: '0.7.9',
		override: /keypress|keydown|keyup/g,
		triggersMap: {},

		specialKeys: { 27: 'esc', 9: 'tab', 32:'space', 13: 'return', 8:'backspace', 145: 'scroll',
			20: 'capslock', 144: 'numlock', 19:'pause', 45:'insert', 36:'home', 46:'del',
			35:'end', 33: 'pageup', 34:'pagedown', 37:'left', 38:'up', 39:'right',40:'down',
			109: '-',
			112:'f1',113:'f2', 114:'f3', 115:'f4', 116:'f5', 117:'f6', 118:'f7', 119:'f8',
			120:'f9', 121:'f10', 122:'f11', 123:'f12', 191: '/'},

		shiftNums: { "`":"~", "1":"!", "2":"@", "3":"#", "4":"$", "5":"%", "6":"^", "7":"&",
			"8":"*", "9":"(", "0":")", "-":"_", "=":"+", ";":":", "'":"\"", ",":"<",
			".":">",  "/":"?",  "\\":"|" },

		newTrigger: function (type, combi, callback) {
			// i.e. {'keyup': {'ctrl': {cb: callback, disableInInput: false}}}
			var result = {};
			result[type] = {};
			result[type][combi] = {cb: callback, disableInInput: false};
			return result;
		}
	};
	// add firefox num pad char codes
	//if (jQuery.browser.mozilla){
	// add num pad char codes
	hotkeys.specialKeys = jQuery.extend(hotkeys.specialKeys, { 96: '0', 97:'1', 98: '2', 99:
		'3', 100: '4', 101: '5', 102: '6', 103: '7', 104: '8', 105: '9', 106: '*',
		107: '+', 109: '-', 110: '.', 111 : '/'
	});
	//}

	// a wrapper around of $.fn.find
	// see more at: http://groups.google.com/group/jquery-en/browse_thread/thread/18f9825e8d22f18d
	jQuery.fn.find = function(selector) {
		this.query = selector;
		return jQuery.fn.__find__.apply(this, arguments);
	};

	jQuery.fn.unbind = function (type, combi, fn) {
		if (jQuery.isFunction(combi)) {
			fn = combi;
			combi = null;
		}
		if (combi && typeof combi === 'string') {
			var selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();
			var hkTypes = type.split(' ');
			for (var x = 0; x < hkTypes.length; x++) {
				delete hotkeys.triggersMap[selectorId][hkTypes[x]][combi];
			}
		}
		// call jQuery original unbind
		return  this.__unbind__(type, fn);
	};

	jQuery.fn.bind = function(type, data, fn) {
		// grab keyup,keydown,keypress
		var handle = type.match(hotkeys.override);

		/*
		 * fixed by Benny 2010/11/16
		 * reson: conflict with jquery 1.4.4
		 * old: if (jQuery.isFunction(data) || !handle){
		 */
		if (!data || jQuery.isFunction(data) || !handle) {
			// call jQuery.bind only
			return this.__bind__(type, data, fn);
		} else {
			// split the job
			var result = null,
				// pass the rest to the original $.fn.bind
				pass2jq = jQuery.trim(type.replace(hotkeys.override, ''));

			// see if there are other types, pass them to the original $.fn.bind
			if (pass2jq) {
				result = this.__bind__(pass2jq, data, fn);
			}

			if (typeof data === "string") {
				data = {'combi': data};
			}
			if (data.combi) {
				for (var x = 0; x < handle.length; x++) {
					var eventType = handle[x];
					var combi = data.combi.toLowerCase(),
						trigger = hotkeys.newTrigger(eventType, combi, fn),
						selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();

					//trigger[eventType][combi].propagate = data.propagate;
					trigger[eventType][combi].disableInInput = data.disableInInput;

					// first time selector is bounded
					if (!hotkeys.triggersMap[selectorId]) {
						hotkeys.triggersMap[selectorId] = trigger;
						//added by frank.zhang
						result = this.__bind__(handle.join(' '), data, hotkeys.handler);
					}
					// first time selector is bounded with this type
					else if (!hotkeys.triggersMap[selectorId][eventType]) {
						hotkeys.triggersMap[selectorId][eventType] = trigger[eventType];
						//added by frank.zhang
						result = this.__bind__(handle.join(' '), data, hotkeys.handler);
					} else {
						//added by frank.zhang
						result = true;
					}
					// make trigger point as array so more than one handler can be bound
					var mapPoint = hotkeys.triggersMap[selectorId][eventType][combi];
					if (!mapPoint) {
						hotkeys.triggersMap[selectorId][eventType][combi] = [trigger[eventType][combi]];
					}
					else if (mapPoint.constructor !== Array) {
						hotkeys.triggersMap[selectorId][eventType][combi] = [mapPoint];
					}
					else {
						hotkeys.triggersMap[selectorId][eventType][combi][mapPoint.length] = trigger[eventType][combi];
					}

					// add attribute and call $.event.add per matched element
					this.each(function() {
						// jQuery wrapper for the current element
						var jqElem = jQuery(this);

						//modified by frank.zhang 2011.4.27
						//fix the js error with jquery 1.4.4
						if (this.nodeType == 9) {
							// element already associated with another collection
							if (this['hkId'] && this['hkId'] !== selectorId) {
								selectorId = this['hkId'] + ";" + selectorId;
							}
							this['hkId'] = selectorId;
						} else {
							// element already associated with another collection
							if (jqElem.attr('hkId') && jqElem.attr('hkId') !== selectorId) {
								selectorId = jqElem.attr('hkId') + ";" + selectorId;
							}
							jqElem.attr('hkId', selectorId);
						}
					});
					//removed by frank.zhang
					//result = this.__bind__(handle.join(' '), data, hotkeys.handler);
				}
			}
			return result;
		}
	};
	// work-around for opera and safari where (sometimes) the target is the element which was last
	// clicked with the mouse and not the document event it would make sense to get the document
	hotkeys.findElement = function (elem) {
		if (!jQuery(elem).attr('hkId')) {
			if (jQuery.browser.opera || jQuery.browser.safari) {
				while (!jQuery(elem).attr('hkId') && elem.parentNode) {
					elem = elem.parentNode;
				}
			}
		}
		return elem;
	};

	// the event handler
	hotkeys.handler = function(event) {
		var target = hotkeys.findElement(event.currentTarget), jTarget = jQuery(target), ids = jTarget.attr('hkId');

		if (ids) {
			ids = ids.split(';');
			var code = event.which, type = event.type, special = hotkeys.specialKeys[code],
				// prevent f5 overlapping with 't' (or f4 with 's', etc.)
				character = !special && String.fromCharCode(code).toLowerCase(), shift = event.shiftKey, ctrl = event.ctrlKey,
				// patch for jquery 1.2.5 && 1.2.6 see more at:
				// http://groups.google.com/group/jquery-en/browse_thread/thread/83e10b3bb1f1c32b
				alt = event.altKey || event.originalEvent.altKey, mapPoint = null;

			for (var x = 0; x < ids.length; x++) {
				if (hotkeys.triggersMap[ids[x]][type]) {
					mapPoint = hotkeys.triggersMap[ids[x]][type];
					break;
				}
			}

			//find by: id.type.combi.options
			if (mapPoint) {
				var trigger;
				// event type is associated with the hkId
				if (!shift && !ctrl && !alt) { // No Modifiers
					trigger = mapPoint[special] || (character && mapPoint[character]);
				}
				else {
					// check combinations (alt|ctrl|shift+anything)
					var modif = '';
					if (alt) modif += 'alt+';
					if (ctrl) modif += 'ctrl+';
					if (shift) modif += 'shift+';
					// modifiers + special keys or modifiers + character or modifiers + shift character or just shift character
					trigger = mapPoint[modif + special];
					if (!trigger) {
						if (character) {
							trigger = mapPoint[modif + character]
								|| mapPoint[modif + hotkeys.shiftNums[character]]
								// '$' can be triggered as 'Shift+4' or 'Shift+$' or just '$'
								|| (modif === 'shift+' && mapPoint[hotkeys.shiftNums[character]]);
						}
					}
				}
				if (trigger) {
					var result = false;
					for (var i = 0; i < trigger.length; i++) {
						if (trigger[i].disableInInput) {
							// double check event.currentTarget and event.target
							var elem = jQuery(event.target);
							if (jTarget.is("input") || jTarget.is("textarea") || jTarget.is("select")
								|| elem.is("input") || elem.is("textarea") || elem.is("select")) {
								return true;
							}
						}
						// call the registered callback function
						result = result || trigger[i].cb.apply(this, [event]);
					}
					return result;
				}
			}
		}
	};
	// place it under window so it can be extended and overridden by others
	window.hotkeys = hotkeys;
	return jQuery;
})(jQuery);
